package cn.funeralobjects.common.jpa.repository;

import cn.funeralobjects.common.annotations.ExistsDelSalt;
import cn.funeralobjects.common.converter.ValueOf;
import cn.funeralobjects.common.model.CreateTime;
import cn.funeralobjects.common.model.DelSalt;
import cn.funeralobjects.common.model.Enable;
import cn.funeralobjects.common.model.UpdateTime;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.data.domain.Example;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.data.jpa.provider.PersistenceProvider;
import org.springframework.data.jpa.repository.query.QueryUtils;
import org.springframework.data.jpa.repository.support.JpaEntityInformation;
import org.springframework.data.jpa.repository.support.JpaEntityInformationSupport;
import org.springframework.data.jpa.repository.support.SimpleJpaRepository;
import org.springframework.data.util.ProxyUtils;
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.Assert;

import javax.persistence.EntityManager;
import javax.persistence.TypedQuery;
import javax.persistence.criteria.*;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.*;

import static org.springframework.data.jpa.repository.query.QueryUtils.COUNT_QUERY_STRING;
import static org.springframework.data.jpa.repository.query.QueryUtils.getQueryString;

/**
 * @author FuneralObjects
 * Create date: 2019-06-17 09:54
 */
@Repository
@Transactional(readOnly = true)
public class BaseDelSaltJpaRepository<T, ID> extends SimpleJpaRepository<T, ID> implements BaseStatusJpaRepository<T, ID> {

    /**
     * 默认一次性分批保存条数
     */
    private static final Integer DEFAULT_BATCH_SIZE = 700;
    private final EntityManager entityManager;
    private final JpaEntityInformation<T, ?> entityInformation;
    private final PersistenceProvider provider;


    /**
     * 实体类class
     */
    protected Class<T> entityClass;

    protected boolean hasEnable;
    protected boolean hasDelSalt;
    protected boolean hasCreateTime;
    protected boolean hasUpdateTime;

    /**
     * 默认值
     */
    private Object existsDelSaltValue;

    public BaseDelSaltJpaRepository(JpaEntityInformation<T, ?> entityInformation, EntityManager entityManager) {
        super(entityInformation, entityManager);
        this.entityManager = entityManager;
        this.entityInformation = entityInformation;
        this.provider = PersistenceProvider.fromEntityManager(entityManager);
        this.initInfo();
    }

    public BaseDelSaltJpaRepository(Class<T> domainClass, EntityManager entityManager) {
        this(JpaEntityInformationSupport.getEntityInformation(domainClass, entityManager), entityManager);
    }


    @Transactional(rollbackFor = Exception.class)
    @Override
    public <S extends T> S save(S entity) {
        if (hasDelSalt) {
            setDelSalt(entity, existsDelSaltValue);
        }
        return super.save(entity);
    }

    @Transactional(rollbackFor = Exception.class)

    @Override
    public <S extends T> S saveAndFlush(S entity) {
        if (hasDelSalt) {
            setDelSalt(entity, existsDelSaltValue);
        }
        return super.saveAndFlush(entity);
    }

    @Transactional(rollbackFor = Exception.class)

    @Override
    public <S extends T> List<S> saveAll(Iterable<S> entities) {
        if (hasDelSalt) {
            entities.forEach(entity -> this.setDelSalt(entity, existsDelSaltValue));
        }
        return super.saveAll(entities);
    }

    @Transactional(rollbackFor = Exception.class)
    @Override
    public List<T> batchSaveAll(Iterable<T> entities) {
        return batchSaveOrUpdateAll(entities, true);
    }

    @Transactional(rollbackFor = Exception.class)
    @Override
    public List<T> batchUpdateAll(Iterable<T> entities) {
        return batchSaveOrUpdateAll(entities, false);
    }

    @Transactional(rollbackFor = Exception.class)
    @Override
    public void disable(ID id) {
        if (!hasEnable) {
            throw new UnsupportedOperationException("This entity[" + entityClass + "] cannot support disable method.");
        }
        this.findById(id).ifPresent(entity -> this.setEnable(entity, false));
    }

    @Transactional(rollbackFor = Exception.class)
    @Override
    public void enable(ID id) {
        if (!hasEnable) {
            throw new UnsupportedOperationException("This entity[" + entityClass + "] cannot support enable method.");
        }
        this.findById(id).ifPresent(entity -> this.setEnable(entity, true));
    }


    @Override
    public Optional<T> findById(ID id) {
        if (!hasDelSalt) {
            return super.findById(id);
        }
        ByIdsSpecification<T> specification = new ByIdsSpecification<>(entityInformation, existsDelSaltValue);
        TypedQuery<T> query = getQuery(specification, Sort.unsorted());
        return Optional.ofNullable(query.setParameter(specification.parameter, Collections.singleton(id)).getSingleResult());
    }


    @Override
    public <S extends T> List<S> findAll(Example<S> example) {
        if (hasDelSalt) {
            setDelSaltExistExample(example);
        }
        return super.findAll(example);
    }


    @Override
    public <S extends T> List<S> findAll(Example<S> example, Sort sort) {
        if (hasDelSalt) {
            setDelSaltExistExample(example);
        }
        return super.findAll(example, sort);
    }


    @Override
    public <S extends T> Page<S> findAll(Example<S> example, Pageable pageable) {
        if (hasDelSalt) {
            setDelSaltExistExample(example);
        }
        return super.findAll(example, pageable);
    }

    @SuppressWarnings("unchecked")

    @Override
    public List<T> findAll() {
        if (hasDelSalt) {
            CriteriaBuilder builder = entityManager.getCriteriaBuilder();
            CriteriaQuery<T> query = builder.createQuery(getDomainClass());
            Root<T> root = query.from(getDomainClass());
            query.select(root);
            query.where(getDelSaltExistSpec().toPredicate((Root<DelSalt<Object>>) root, query, builder));
            return entityManager.createQuery(query).getResultList();
        }
        return super.findAll();
    }


    @Override
    public List<T> findAll(Specification<T> spec) {
        if (hasDelSalt) {
            andDelSalt(spec);
        }
        return super.findAll(spec);
    }


    @Override
    public List<T> findAllById(Iterable<ID> ids) {
        if (!ids.iterator().hasNext()) {
            return Collections.emptyList();
        }
        ByIdsSpecification<T> specification = new ByIdsSpecification<>(entityInformation, existsDelSaltValue);
        TypedQuery<T> query = getQuery(specification, Sort.unsorted());
        return query.setParameter(specification.parameter, ids).getResultList();
    }

    @SuppressWarnings("unchecked")

    @Override
    public List<T> findAll(Sort sort) {
        if (hasDelSalt) {
            return super.findAll((Specification<T>) getDelSaltExistSpec(), sort);
        }
        return super.findAll(sort);
    }

    @SuppressWarnings("unchecked")

    @Override
    public Page<T> findAll(Pageable pageable) {
        if (hasDelSalt) {
            return super.findAll((Specification<T>) getDelSaltExistSpec(), pageable);
        }
        return super.findAll(pageable);
    }


    @Override
    public Optional<T> findOne(Specification<T> spec) {
        if (hasDelSalt) {
            andDelSalt(spec);
        }
        return super.findOne(spec);
    }


    @Override
    public Page<T> findAll(Specification<T> spec, Pageable pageable) {
        if (hasDelSalt) {
            andDelSalt(spec);
        }
        return super.findAll(spec, pageable);
    }


    @Override
    public List<T> findAll(Specification<T> spec, Sort sort) {
        if (hasDelSalt) {
            andDelSalt(spec);
        }
        return super.findAll(spec, sort);
    }


    @Override
    public <S extends T> Optional<S> findOne(Example<S> example) {
        if (hasDelSalt) {
            setDelSaltExistExample(example);
        }
        return super.findOne(example);
    }

    @Override
    public boolean existsById(ID id) {
        if (id == null) {
            throw new IllegalArgumentException("The arg of id cannot be null.");
        }

        if (entityInformation.getIdAttribute() == null) {
            return findById(id).isPresent();
        }

        String placeholder = provider.getCountQueryPlaceholder();
        String entityName = entityInformation.getEntityName();
        Iterable<String> idAttributeNames = entityInformation.getIdAttributeNames();
        String existsQuery = QueryUtils.getExistsQueryString(entityName, placeholder, idAttributeNames);

        if (hasDelSalt) {
            existsQuery += " AND delSalt = " + existsDelSaltValue;
        }

        TypedQuery<Long> query = entityManager.createQuery(existsQuery, Long.class);

        if (!entityInformation.hasCompositeId()) {
            query.setParameter(idAttributeNames.iterator().next(), id);
            return query.getSingleResult() == 1L;
        }

        for (String idAttributeName : idAttributeNames) {

            Object idAttributeValue = entityInformation.getCompositeIdAttributeValue(id, idAttributeName);

            boolean complexIdParameterValueDiscovered = idAttributeValue != null
                    && !query.getParameter(idAttributeName).getParameterType().isAssignableFrom(idAttributeValue.getClass());

            if (complexIdParameterValueDiscovered) {

                // fall-back to findById(id) which does the proper mapping for the parameter.
                return findById(id).isPresent();
            }

            query.setParameter(idAttributeName, idAttributeValue);
        }

        return query.getSingleResult() == 1L;
    }

    @Override
    public <S extends T> boolean exists(Example<S> example) {
        if (hasDelSalt) {
            setDelSaltExistExample(example);
        }
        return super.exists(example);
    }

    @Override
    public long count() {
        return entityManager.createQuery(getCountQueryString(), Long.class).getSingleResult();
    }

    @Override
    public long count(Specification<T> spec) {
        if (hasDelSalt) {
            andDelSalt(spec);
        }
        return super.count(spec);
    }

    @Override
    public <S extends T> long count(Example<S> example) {
        if (hasDelSalt) {
            setDelSaltExistExample(example);
        }
        return super.count(example);
    }


    @Override
    public void deleteById(ID id) {
        if (hasDelSalt) {
            T entity = this.findById(id).orElseThrow(() -> new EmptyResultDataAccessException(String.format("No %s entity with id %s exists!", entityInformation.getJavaType(), id), 1));
            setDeleteDelSalt(entity);
            this.save(entity);
            return;
        }
        super.deleteById(id);
    }

    @SuppressWarnings("unchecked")
    @Override
    public void delete(T entity) {
        if (hasDelSalt) {
            if (entityInformation.isNew(entity)) {
                return;
            }
            Class<?> type = ProxyUtils.getUserClass(entity);
            T existing = (T) entityManager.find(type, entityInformation.getId(entity));
            if (existing == null) {
                return;
            }
            setDeleteDelSalt(entity);
            this.save(entity);
            return;
        }
        super.delete(entity);
    }

    @Override
    public void deleteAll(Iterable<? extends T> entities) {
        Assert.notNull(entities, "Entities must not be null!");
        for (T entity : entities) {
            this.delete(entity);
        }
    }

    @Override
    public void deleteInBatch(Iterable<T> entities) {
        if (hasDelSalt) {
            throw new UnsupportedOperationException("The has delSalt entity doesn't support the method of [deleteInBatch].");
        }
        super.deleteInBatch(entities);
    }

    @Override
    public void deleteAll() {
        if (hasDelSalt) {
            throw new UnsupportedOperationException("The has delSalt entity doesn't support the method of [deleteAll].");
        }
        super.deleteAll();
    }

    @Override
    public void deleteAllInBatch() {
        if (hasDelSalt) {
            throw new UnsupportedOperationException("The has delSalt entity doesn't support the method of [deleteAllInBatch].");
        }
        super.deleteAllInBatch();
    }

    private String getCountQueryString() {
        String countQuery = String.format(COUNT_QUERY_STRING, provider.getCountQueryPlaceholder(), "%s");
        if (hasDelSalt) {
            countQuery += " WHERE delSalt = " + existsDelSaltValue;
        }
        return getQueryString(countQuery, entityInformation.getEntityName());
    }

    private static final class ByIdsSpecification<T> implements Specification<T> {

        private static final long serialVersionUID = 1L;

        private final JpaEntityInformation<T, ?> entityInformation;
        @Nullable
        ParameterExpression<Iterable> parameter;
        private Object existsDelSalt;

        ByIdsSpecification(JpaEntityInformation<T, ?> entityInformation, Object existsDelSalt) {
            this.entityInformation = entityInformation;
            this.existsDelSalt = existsDelSalt;
        }

        /*
         * (non-Javadoc)
         * @see org.springframework.data.jpa.domain.Specification#toPredicate(javax.persistence.criteria.Root, javax.persistence.criteria.CriteriaQuery, javax.persistence.criteria.CriteriaBuilder)
         */
        @Override
        public Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
            Path<?> path = root.get(entityInformation.getIdAttribute());
            parameter = cb.parameter(Iterable.class);
            return cb.and(path.in(parameter), cb.equal(root.get("delSalt"), existsDelSalt));
        }
    }


    @SuppressWarnings("unchecked")
    private void initInfo() {
        ParameterizedType type = (ParameterizedType) this.getClass().getGenericSuperclass();
        Type[] types = type.getActualTypeArguments();
        entityClass = (Class<T>) types[0];
        System.out.println(this.getDomainClass());
        this.hasEnable = Enable.class.isAssignableFrom(entityClass);
        this.hasDelSalt = DelSalt.class.isAssignableFrom(entityClass);
        this.hasCreateTime = CreateTime.class.isAssignableFrom(entityClass);
        this.hasUpdateTime = UpdateTime.class.isAssignableFrom(entityClass);

        if (this.hasDelSalt) {
            ExistsDelSalt existsDelSalt = entityClass.getAnnotation(ExistsDelSalt.class);
            if (existsDelSalt == null) {
                throw new RuntimeException("The entity[" + entityClass + "] lack an annotation of @ExistsDelSalt. ");
            }
            try {
                ValueOf<?> valueOf = existsDelSalt.valueOf().getDeclaredConstructor().newInstance();
                this.existsDelSaltValue = valueOf.valueOf(existsDelSalt.existValue());
            } catch (ReflectiveOperationException e) {
                throw new RuntimeException(e);
            }

        }
    }


//    private T setExistsDelSalt(T entity){
//        if(!hasDelSalt){
//            return setDelSalt(entity, existsDelSaltValue);
//        }
//        return entity;
//    }


    private void setEnable(T entity, boolean enable) {
        Enable be = ((Enable) entity);
        be.setEnable(enable);
    }

    @SuppressWarnings("unchecked")
    private void setDelSalt(T entity, Object delSalt) {
        DelSalt<Object> obj = (DelSalt<Object>) entity;
        obj.setDelSalt(delSalt);
    }

//    private T setCreateTime(T entity, Date createTime){
//        CreateTime obj = (CreateTime) entity;
//        obj.setCreateTime(createTime);
//        return entity;
//    }
//
//    private T setUpdateTime(T entity, Date updateTime){
//        UpdateTime obj = (UpdateTime) entity;
//        obj.setUpdateTime(updateTime);
//        return entity;
//    }

    private List<T> batchSaveOrUpdateAll(Iterable<T> entities, boolean save) {
        Iterator<T> iterator = entities.iterator();
        List<T> result = new ArrayList<>();
        int index = 0;
        while (iterator.hasNext()) {
            index++;
            T entity = iterator.next();
            if (hasDelSalt) {
                setDelSalt(entity, existsDelSaltValue);
            }
            if (save) {
                entityManager.persist(entity);
            } else {
                entityManager.merge(entity);
            }
            if (index % DEFAULT_BATCH_SIZE == 0) {
                entityManager.flush();
                entityManager.clear();
            }
            result.add(entity);
        }
        if (index % DEFAULT_BATCH_SIZE != 0) {
            entityManager.flush();
            entityManager.clear();
        }
        return result;
    }

    /**
     * 获取生成一个新的AbstractStatusEntity的查询条件
     *
     * @return 查询结果
     */
    private Specification<DelSalt<Object>> getDelSaltExistSpec() {
        return (Specification<DelSalt<Object>>) (root, query, criteriaBuilder) ->
                criteriaBuilder.equal(root.get("delSalt"), existsDelSaltValue);
    }

    @SuppressWarnings("unchecked")
    private void andDelSalt(Specification<T> spec) {
        ((Specification<DelSalt<Object>>) spec).and(getDelSaltExistSpec());
    }

    @SuppressWarnings("unchecked")
    private void setDelSaltExistExample(Example<?> example) {
        DelSalt<Object> delSalt = (DelSalt<Object>) (example.getProbe());
        if (delSalt.getDelSalt() == null) {
            delSalt.setDelSalt(existsDelSaltValue);
        }
    }

    /**
     * 设置删除标记
     *
     * @param entity 实体对象
     */
    @SuppressWarnings("unchecked")
    private void setDeleteDelSalt(T entity) {
        if (hasDelSalt) {
            DelSalt<Object> be = ((DelSalt<Object>) entity);
            be.setDelSalt(be.generatorDelSalt());
        }
    }

}
